從今天開始,我們要賦予我們的 App思考的能力。我們要讓它不再只是一個工具,而是一個能與我們對話的夥伴。
困惑點:我的 App 要怎麼跟「外面」的世界說話?API 聽起來是在「雲端」或「網路上」的一個東西。那我的 App 要怎麼跟它溝通?
AI 協作
我之前只會寫在手機裡運行的程式。現在我需要讓 App 去跟網路上的 AI API 拿資料,我不知道第一步該怎麼做。請問在 Swift 中,要如何處理這種『需要去網路上拿東西回來』的任務?請簡單說明並不需要給程式碼
AI 的回答:
您好,很高興能為您解答關於 App 與網路 API 溝通的問題。您提到的「需要去網路上拿東西回來」的任務,在程式設計中稱為非同步(Asynchronous)操作。
為什麼需要非同步?
簡單來說,手機 App 的使用者介面(UI)是在一個稱為主執行緒(Main Thread)的單一執行緒上運作的。如果我們直接在這個主執行緒上執行網路請求,程式會因為等待資料回傳而卡住,導致 UI 凍結,使用者會看到 App 完全沒有反應,這會帶來非常糟糕的使用者體驗。
為了避免這種情況,我們需要將耗時的任務(例如網路請求)放到背景執行緒(Background Thread)去執行,讓主執行緒可以繼續處理 UI 更新和使用者互動。當背景任務完成後,再通知主執行緒去處理回傳的資料。
Swift 的非同步處理方式
在 Swift 中,處理這種非同步任務最現代且推薦的方式就是
async/await
。
async
關鍵字:您可以在函式(Function)或方法(Method)前面加上async
,這表示這個函式是一個非同步函式,它在執行過程中可能會被「暫停」(suspend),等待某些耗時的操作完成,例如網路請求。await
關鍵字:當您在呼叫一個async
函式時,您需要使用await
關鍵字。await
會暫停當前程式碼的執行,直到它所呼叫的async
函式完成並回傳結果。這就是async/await
語法的核心概念:它讓非同步程式碼看起來像是循序執行(Synchronous),大大提升了可讀性。舉例來說,當您發起一個網路請求時,
await
會暫停執行,讓您的程式可以去處理其他任務,當資料回傳後,程式會從await
的地方繼續執行,去處理回來的資料。這種機制讓您不必手動處理複雜的回呼函式(Completion Handlers)或巢狀結構,使程式碼更簡潔、易於維護,也降低了出現錯誤的機率。
網路請求的流程概觀
要讓 App 去跟網路上的 AI API 拿資料,大致的步驟會是:
- 建立 URLRequest:設定好 API 的網址(URL)、請求方法(GET/POST 等),以及需要的請求頭(Headers)和請求主體(Body)。
- 執行網路請求:使用像是
URLSession
這類的 API,發起您的async
網路請求。- 使用
await
等待結果:在您呼叫網路請求函式時,使用await
等待資料回傳。- 處理回傳資料:當資料回傳後,您會收到一個
Data
物件,通常是 JSON 格式。您需要將這個Data
解析(Decode)成 App 中可用的資料模型(Data Model)。這個過程也通常需要使用async/await
。如果您有進一步的疑問,或是想深入了解如何將
async/await
應用在具體程式碼中,我會樂意為您提供更詳細的說明。
根據 AI 的建議,我把詳細的實作步驟整理如下:
NetworkManager
:打造一個可重複使用的網路請求底層,負責發送所有請求。GeminiAPIService
:建立一個專門與 Gemini API 溝通的服務,負責處理所有與 Gemini 相關的邏輯。ViewController
中,用最簡潔的方式呼叫服務,並將結果顯示在畫面上。NetworkManager
專業的 App 不會把 URLSession
的程式碼散落在各處。我們會建立一個通用的網路管理員,並搭配一個自訂的錯誤型別 APPError
,讓錯誤處理更清晰。
檔案:APPError.swift
(新增)
import Foundation
enum APPError: Error {
case invalidURL
case serverError
case decodingError
// ... 可根據需要新增更多錯誤類型
}
檔案:NetworkManager.swift
(新增)
import Foundation
class NetworkManager {
static let shared = NetworkManager()
private init() {}
func request<T: Decodable>(endpoint: String, method: String = "GET", headers: [String: String]? = nil, body: Data? = nil) async throws -> T {
guard let url = URL(string: endpoint) else {
throw APPError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = method
request.allHTTPHeaderFields = headers
request.httpBody = body
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
throw APPError.serverError
}
do {
let decodedData = try JSONDecoder().decode(T.self, from: data)
return decodedData
} catch {
throw APPError.decodingError
}
}
}
GeminiAPIService
有了通用的 NetworkManager
,我們接著建立一個專門負責與 Gemini API 溝通的 Service
。
檔案:GeminiDataModels.swift
(新增)
import Foundation
// --- 用於建立請求 (Request) 的資料結構 ---
struct GeminiRequest: Codable {
let contents: [Content]
}
struct Content: Codable {
let parts: [Part]
}
struct Part: Codable {
var text: String? = nil
var inlineData: InlineData? = nil
}
struct InlineData: Codable {
let mimeType: String
let data: String
}
// --- 用於解析回應 (Response) 的資料結構 ---
struct GeminiResponse: Codable {
let candidates: [Candidate]
}
struct Candidate: Codable {
let content: Content
}
檔案:GeminiAPIService.swift
(新增)
import Foundation
class GeminiAPIService {
private let networkManager = NetworkManager.shared
private let apiKey: String
init() {
// 從 API_KEY.plist 安全地讀取金鑰
guard let path = Bundle.main.path(forResource: "API_KEY", ofType: "plist"),
let dict = NSDictionary(contentsOfFile: path),
let key = dict["GeminiAPIKey"] as? String else {
fatalError("無法在 API_KEY.plist 中找到 GeminiAPIKey")
}
self.apiKey = key
}
func generateContent(prompt: String) async throws -> GeminiResponse {
let endpoint = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent"
let headers = ["Content-Type": "application/json", "x-goog-api-key": apiKey]
let requestBody = GeminiRequest(contents: [Content(parts: [Part(text: prompt)])])
let bodyData = try JSONEncoder().encode(requestBody)
let response: GeminiResponse = try await networkManager.request(
endpoint: endpoint,
method: "POST",
headers: headers,
body: bodyData
)
return response
}
}
ViewController
中呼叫檔案:AITestViewController.swift
(新增)
import UIKit
class AITestViewController: UIViewController {
// MARK: - IBOutlet
@IBOutlet weak var btnSent: UIButton!
@IBOutlet weak var txfResult: UITextView!
// MARK: - Property
private let geminiService = GeminiAPIService()
// MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - UI Setting
// MARK: - IBAcion
@IBAction func sendTapped(_ sender: Any) {
txfResult.text = "AI 正在思考中..."
Task {
do {
let response = try await geminiService.generateContent(prompt: "用繁體中文講一句有關於Swift的冷知識")
if let aiText = response.candidates.first?.content.parts.first?.text {
await MainActor.run {
txfResult.text = aiText
}
}
} catch {
await MainActor.run {
txfResult.text = "發生錯誤: \(error.localizedDescription)"
}
}
}
}
// MARK: - Function
}
// MARK: - Extensions
!!務必燒到實體機使用,不要用虛擬機測試!!因為 iOS 模擬器本身就是一台虛擬機,在虛擬機裡執行它會造成巢狀虛擬,導致網路功能完全失效而無法連線。
今天,我們成功地與 AI 進行了第一次對話,我們建立了職責分離的 NetworkManager
和 GeminiAPIService
,讓 ViewController
保持整潔。
明天,我們將基於這個堅實的基礎,打造第一個完整的 AI 專案:一個能將文字轉換成顏色的智慧配色 App